package info.magnolia.blossom.sample.cv.service.impl;

import info.magnolia.blossom.sample.cv.service.MapperService;
import info.magnolia.jcr.util.SessionUtil;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang.WordUtils;
import org.apache.log4j.Logger;

import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.RepositoryException;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Created with IntelliJ IDEA.
 * User: peterbalazs
 * Date: 2/22/13
 * Time: 2:10 PM
 * To change this template use File | Settings | File Templates.
 */
public class MapperServiceImpl implements MapperService {

    /**
     * Class logger.
     */
    private static final Logger logger = Logger.getLogger(MapperServiceImpl.class);

    @Override
    public void storeObject(final String repository, final String relPath, final Serializable object) throws RepositoryException{
        final Node node = SessionUtil.getNode(repository, relPath);
        if (node == null) {
            throw new RepositoryException("No node found under " + repository + relPath);
        }

        logger.info("Node found with type " + node.getPrimaryNodeType().getName());

        storeObject(node, object);
    }

    @Override
    public void storeObject(final Node node, final Serializable object) throws RepositoryException {
        if (node == null) {
            throw new NullPointerException("Argument node is null");
        }
        if (object == null) {
            throw new NullPointerException("Object to be stored is null");
        }

        try {
            final Map<String, Object> properties = describe(object);
            for (Map.Entry<String, Object> entry : properties.entrySet()) {
                storeProperty(node, entry.getKey(), entry.getValue());
            }
            node.getSession().save();
        } catch (Exception e) {
            logger.error("Error storing properties", e);
            throw new RepositoryException("Error storing properties", e);
        }
    }

    @Override
    public Serializable getObject(final String repository, final String relPath, final Class<? extends Serializable> objectClass) throws RepositoryException, InstantiationException {
        final Node node = SessionUtil.getNode(repository, relPath);
        if (node == null) {
            throw new RepositoryException("No node found under " + repository + relPath);
        }

        logger.info("Node found with type " + node.getPrimaryNodeType().getName());

        return getObject(node, objectClass);
    }

    @Override
    public Serializable getObject(final Node node, final Class<? extends Serializable> objectClass) throws RepositoryException, InstantiationException {
        if (node == null) {
            throw new NullPointerException("Node argument is null");
        }
        if (objectClass == null) {
            throw new NullPointerException("Object class is null, don't know what type of class to instantiate");
        }

        final Serializable object;
        try {
            object = objectClass.newInstance();
        } catch (Exception e) {
            logger.error("Error instantiating class of type " + objectClass.getCanonicalName());
            throw new InstantiationException("Error instantiating class of type " + objectClass.getCanonicalName());
        }

        if (object == null) {
            return null; // should never get here
        }

        final PropertyIterator iterator = node.getProperties();
        while (iterator.hasNext()) {
            final Property property = iterator.nextProperty();
            addProperty(node, property, object);
        }

        return object;
    }

    private void storeProperty(final Node node, final String propertyName, final Object value) throws RepositoryException {
        // TODO handle multiple types
        if (value == null) {
            return;
        }
        if ("class".equalsIgnoreCase(propertyName)) {
            return;
        }
        if (value instanceof Date) {
            // store the timestamp
            node.setProperty(propertyName, ((Date) value).getTime() + "");
        } else {
            node.setProperty(propertyName, value.toString());
        }
    }

    private void addProperty(final Node node, final Property property, final Serializable object) throws RepositoryException {
        if (property.isNode()) {
            // TODO handle this case
            return;
        } else {
            Object value = property.getString();
            PropertyDescriptor pd;
            try {
                pd = new PropertyDescriptor(property.getName(), object.getClass());
                if (pd.getPropertyType().equals(Date.class)) {
                    final Date date = new Date();
                    date.setTime(Long.parseLong(property.getString()));
                    BeanUtils.setProperty(object, property.getName(), date);
                    return;
                }
            } catch (Exception e) {
                logger.warn("Unable to determine type of property " + property.getName(), e);
            }
            try {
                // TODO handle multiple value types
                BeanUtils.setProperty(object, property.getName(), value);
            } catch (Exception e) {
                logger.warn("Tried to set property " + property.getName() + " but failed", e);
            }
        }
    }

    private static Map<String, Object> describe(final Serializable bean) throws Exception {
        final Map<String, Object> propertyMap = new HashMap<String, Object>();
        for (Method method : bean.getClass().getMethods()) {
            final String name = method.getName();
            if (name.startsWith("get") && !name.startsWith("getClass")) {
                try {
                    final Object value = method.invoke(bean);
                    final String propertyName = WordUtils.uncapitalize(name.substring(3));
                    propertyMap.put(propertyName, value);
                } catch (Exception e) {
                    // ignore
                }
            } else if (name.startsWith("is")) {
                try {
                    final Boolean value = (Boolean) method.invoke(bean);
                    final String propertyName = WordUtils.uncapitalize(name.substring(2));
                    propertyMap.put(propertyName, value);
                } catch (Exception e) {
                    // ignore
                }
            }
        }
        return propertyMap;
    }
}
